# This file is part of Neuroinfo Toolkit.
#
# Neuroinfo Toolkit is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Neuroinfo Toolkit is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Neuroinfo Toolkit.  If not, see <http://www.gnu.org/licenses/>.

import os
import re
import sys
import time
import socket
import string
import smtplib
import getpass
import platform
from time import strftime
import neuro.config as config
from neuro.strings import regex
from neuro.command import Command
from email.MIMEText import MIMEText
import neuro.filesystem as filesystem
from neuro.exceptions import EmailException
from neuro.exceptions import BaseException
from neuro.exceptions import UnsupportedException
from neuro.exceptions import IllegalArgumentException
from neuro.exceptions import CommandNotFoundException

## --- setup timezone
os.environ["TZ"] = config.timezone

## --- condition is for Windows compat
if(hasattr(time, "tzset")):
    time.tzset()

def exit(status=0):
    '''
    Exit program ::
    
        >>> exit(1)
        
    :param status:
    :type status: int
    '''
    if(not isinstance(status, int)):
        raise IllegalArgumentException("Status must be an instance of int")
    
    sys.exit(status)
    
def getHostname():
    '''
    Retrieve hostname ::
        
        >>> hostname = getHostname()
        'www.domain.org'
        
    :rtype: str
    '''
    return socket.gethostbyaddr(socket.gethostname())[0]

def getOS():
    '''
    Retrieve operating system ::
    
        >>> os = getOS()
        'Linux 3.2.0-24-generic #39-Ubuntu SMP Mon May 21 16:52:17 UTC 2012 x86_64'
    
    :rtype: str
    '''
    return string.join(os.uname(), " ")
    
def email(array, type="plain"):
    '''
    Send email ::

        >>> email({ "to": "foo@bar.com",
                    "from": "your@email.com",
                    "subject": "hey!",
                    "body": "this is a message" })
                
    :param array: Basic email contents 
    :type array: dict
    :raises: :class:`~neuro.exceptions.EmailException`
    '''
    ## --- input validation
    if(not isinstance(array, dict)):
        raise IllegalArgumentException("Array must be an instance of dict")
    elif("to" not in array):
        raise IllegalArgumentException("Array must have key \"to\"")
    elif(not array["to"]):
        raise IllegalArgumentException("Array[\"to\"] must not be empty")
    elif("from" not in array):
        raise IllegalArgumentException("Array must have key \"from\"")
    elif(not array["from"]):
        raise IllegalArgumentException("Array[\"from\"] must not be empty")
    elif("subject" not in array):
        raise IllegalArgumentException("Array must have key \"subject\"")
    elif(not array["subject"]):
        raise IllegalArgumentException("Array[\"subject\"] must not be empty")
    elif("body" not in array):
        raise IllegalArgumentException("Array must have key \"body\"")
    elif(not array["body"]):
        raise IllegalArgumentException("Array[\"body\"] must not be empty")
    
    message = MIMEText(array["body"], type, "ISO-8859-1")
    message["From"] = array["from"]
    message["To"] = array["to"]
    message["Subject"] = array["subject"]

    try:
        s = smtplib.SMTP("localhost")
        s.sendmail(array["from"], array["to"], message.__str__())
        s.quit()
    except Exception, e:
        raise EmailException(array, repr(e[1]))
    
def getUsername():
    '''
    Retrieve current username ::
    
        >>> getUsername()
        'jdoe'
    
    :returns: Current user's username
    :rtype: str
    '''
    return getpass.getuser()

def getUID():
    '''
    Retrieve current UID ::
    
        >>> getUID()
        1000
    
    :returns: Current user's UID
    :rtype: int
    '''
    return int(os.getuid())

def getLoad(minutes=1):
    '''
    Retrieve load average over 1, 5 or 15 minutes ::
    
        >>> getLoad(1)
        0.25
        >>> getLoad(5)
        0.36
        >>> getLoad(15)
        0.38
        
    :param minutes: Minutes
    :type minutes: int
    :returns: Load over *N* minutes
    :rtype: float
    '''
    if(minutes == 1):
        return float(os.getloadavg()[0])
    elif(minutes == 5):
        return float(os.getloadavg()[1])
    elif(minutes == 15):
        return float(os.getloadavg()[2])
    else:
        raise IllegalArgumentException("Minutes must be 1, 5 or 15")
    
def getYear():
    '''
    Retrieve the current year ::
    
        >>> getYear()
        '2000'
    
    :return: Today's year
    :rtype: str
    '''
    return strftime("%Y")

def getMonth():
    '''
    Retrieve the current month ::
    
        >>> getMonth()
        '01'
    
    :returns: Today's month
    :rtype: str
    '''
    return strftime("%m")

def getDay():
    '''
    Retrieve the current day ::
    
        >>> getDay()
        '01'
    
    :returns: Today's day
    :rtype: str
    '''
    return strftime("%d")

def getHour():
    '''
    Retrieve the current hour ::
    
        >>> getHour()
        '22'
    
    :returns: Current hour
    :rtype: str
    '''
    return strftime("%H")

def getMinute():
    '''
    Retrieve the current minute ::
    
        >>> getMinute()
        '04'
    
    :returns: Current minute
    :rtype: str
    '''
    return strftime("%M")

def getSecond():
    '''
    Retrieve the current second ::
    
        >>> getSecond()
        '46'
    
    :returns: Current second
    :rtype: str
    '''
    return strftime("%S")

def getEnvironmentVariable(name):
    '''
    Get contents of an environment variable in current context ::

        >>> getEnvironmentVariable("SHELL")
        '/bin/bash'
        
    :param name: Environment variable name
    :type name: str
    :returns: Environment variable value
    :rtype: str
    '''
    if(not isinstance(name, basestring)):
        raise IllegalArgumentException("Environment variable name must be an instance of str")

    name = name.strip()

    if(name == ""):
        raise IllegalArgumentException("Environment variable name cannot be empty")

    if(name not in os.environ):
        return None
    else:
        return os.environ[name]

def setEnvironmentVariable(name, value):
    '''
    Set an environment variable in current context ::

        >>> setEnvironmentVariable("FOO", "bar")
        
    :param name: Environment variable name
    :type name: str
    :param value: Environment variable value
    :type value: str, int, float, ...
    '''
    if(not isinstance(name, basestring)):
        raise IllegalArgumentException("Environment variable name must be an instance of str")
    elif(not isinstance(value, basestring)):
        raise IllegalArgumentException("Environment variable value must be an instance of str")

    name = name.strip()
    value = value.strip()

    if(name == ""):
        raise IllegalArgumentException("Environment variable name cannot be empty")

    if(isinstance(value, basestring)):
        os.environ[name] = value
    else:
        os.environ[name] = value.__str__()

def unsetEnvironmentVariable(name):
    '''
    Unset an environment variable in current context ::

        >>> unsetEnvironmentVariable("FOO")
        
    :param name: Environment variable name
    :type name: str
    '''
    if(not isinstance(name, basestring)):
        raise IllegalArgumentException("Environment variable name must be an instance of str")

    name = name.strip()

    if(name == ""):
        raise IllegalArgumentException("Environment variable name cannot be empty")

    if(name in os.environ):
        del os.environ[name]

def check(binary, package="", return_abspath=False):
    '''
    Check for a system executable ::

        >>> check("cat")
        True
        >>> check("cat", return_abspath=True)
        '/bin/cat'

    :param binary: Name of binary
    :type binary: str
    :param package: Package name to print along with error message
    :type package: str
    :param return_abspath: Return absolute path instead of a simple bool
    :type return_abspath: bool
    :rtype: bool, str
    :raises: :class:`~neuro.command.CommandNotFoundException`
    '''
    ## --- check input parameters
    if(not isinstance(binary, basestring)):
        raise IllegalArgumentException("Binary must be an instance of str")
    elif(not isinstance(package, basestring)):
        raise IllegalArgumentException("Package must be an instance of str")

    binary = binary.strip()

    if(binary == ""):
        raise IllegalArgumentException("Binary cannot be empty")

    (filepath, filename) = os.path.split(binary)

    if(filepath):
        if(filesystem.exists(binary) and filesystem.isExecutable(binary)):
            if return_abspath:
                return binary
            else:
                return True
        else:
                raise CommandNotFoundException(binary, package)
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            exe_file = os.path.join(path, filename)
            if(filesystem.exists(exe_file) and filesystem.isExecutable(exe_file)):
                if return_abspath:
                    return exe_file
                else:
                    return True

    raise CommandNotFoundException(binary, package)

def getMemInfo():
    '''
    Get system memory info (in bytes). Only works on \*nix platforms ::
        
        >>> getMemInfo()
        (8152887296, 6303748096)
    
    :returns: (total, used)
    :rtype: tuple
    :raises: :class:`~neuro.exceptions.UnsupportedException`,
             :class:`~neuro.exceptions.SystemProfileException`
    '''
    if(platform.system() != "Linux"):
        raise UnsupportedException(getMemInfo)

    command = Command("cat /proc/meminfo")
    command.execute()

    ## --- parse the output
    memTotal = regex(".*MemTotal:\s+([0-9]+).*$", command.getStdout())

    if(not isinstance(memTotal, tuple)):
        raise SystemProfileException(SystemProfileException.BLANK, "MemTotal")

    memTotal = memTotal[0]

    if(not re.match(r'[+-]?[0-9]+$', memTotal)):
        raise SystemProfileException(SystemProfileException.TYPE, "MemTotal", memTotal)

    free = regex(".*\nMemFree:\s+([0-9]+).*$", command.getStdout())
    buffers = regex(".*\nBuffers:\s+([0-9]+).*$", command.getStdout())
    cached = regex(".*\nCached:\s+([0-9]+).*$", command.getStdout())
    swapCached = regex(".*\nSwapCached:\s+([0-9]+).*$", command.getStdout())

    memFree = int(free[0]) + int(buffers[0]) + int(cached[0]) + int(swapCached[0])

    if(not re.match(r'[+-]?[0-9]+$', str(memFree))):
        raise SystemProfileException(SystemProfileException.TYPE, "MemFree", memFree)

    ## --- return in bytes, assuming everything is in kB
    return (int(memTotal) * 1024, int(memFree) * 1024)

def getSwapInfo():
    '''
    Get swap info (in bytes). Only works on \*nix platforms ::
    
        >>> getSwapInfo()
        (8367632384, 8367632384)

    :returns: (total, free)
    :rtype: tuple
    :raises: :class:`~neuro.exceptions.UnsupportedException`,
             :class:`~neuro.exceptions.SystemProfileException`
    '''
    if(platform.system() != "Linux"):
        raise UnsupportedException(getSwapInfo)

    command = Command("cat /proc/meminfo")
    command.execute()

    ## --- assume the result is in kB
    swapTotal = regex(".*SwapTotal:\s+([0-9]+).*$", command.getStdout())

    if(not isinstance(swapTotal, tuple)):
        raise SystemProfileException(SystemProfileException.BLANK, "SwapTotal")

    swapTotal = swapTotal[0]

    if(not re.match(r'[+-]?[0-9]+$', swapTotal)):
        raise SystemProfileException(SystemProfileException.TYPE, "SwapTotal", swapTotal)

    swapFree = regex(".*SwapFree:\s+([0-9]+).*$", command.getStdout())

    if(not isinstance(swapFree, tuple)):
        raise SystemProfileException(SystemProfileException.BLANK, "SwapFree")

    swapFree = swapFree[0]

    if(not re.match(r'[+-]?[0-9]+$', swapFree)):
        raise SystemProfileException(SystemProfileException.TYPE, "SwapFree", swapFree)

    return (int(swapTotal) * 1024, int(swapFree) * 1024)

class SystemProfileException(BaseException):
    '''
    System profiling exception
    '''
    BLANK = 0
    TYPE = 1

    def __init__(self, type, key, value=""):
        '''
        Constructor

        :param type:
        :type type: int
        :param key:
        :type key: str
        :param value:
        :type value: int, float, str, ...
        '''
        if(type == None):
            raise IllegalArgumentException("Type must be an instance of int")
        elif(not isinstance(key, basestring)):
            raise IllegalArgumentExcetption("Key must be an instance of str")
        elif(key.strip() == ""):
            raise IllegalArgumentException("Key cannot be empty")
        elif(not isinstance(value, basestring)):
            raise IllegalArgumentException("Value must be an instance of str")

        ## --- check type values
        if(type != 0 and type != 1):
            raise IllegalArgumentException("Type of exception must be SystemProfileException.BLANK (0) or SystemProfileException.TYPE (1)")

        self._type = type
        self._key = key.strip()
        self._value = value.strip()

    def getKey(self):
        '''
        Get profiling key

        :rtype: str
        '''
        return self._key

    def getValue(self):
        '''
        Get profiling value

        :rtype: str
        '''
        return self._value

    def getMessage(self):
        '''
        Get exception message

        :rtype: str
        '''
        if(self._type == SystemProfileException.BLANK):
            return "Value returned for " + self._key + " is blank"
        elif(self._type == SystemProfileException.TYPE):
            return "Value returned for " + self._key + " was of an unexpected typee"
        else:
            return "An unknown system profiling exception occurred"
